Skip to content

Rebuild profiling on the MAUI CLI - #221

Merged
Redth merged 3 commits into
mainfrom
redth-modernize-app-profiling
Jul 29, 2026
Merged

Rebuild profiling on the MAUI CLI#221
Redth merged 3 commits into
mainfrom
redth-modernize-app-profiling

Conversation

@Redth

@Redth Redth commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Sherpa's profiling feature predated maui profile, so it carried its own capture pipeline: prerequisite probing, a capture planner, dsrouter and dotnet-trace orchestration, and a multi-step wizard to collect everything that pipeline needed. The MAUI CLI now does all of that natively, so most of that code was reimplementing something we can just call.

This replaces the custom pipeline with a thin wrapper around maui profile and collapses the wizard into a single dialog.

Capture profile dialog

Approach

The old orchestration layer is gone: ProfilingCaptureOrchestrationService, ProfilingPrerequisitesService, ProfilingSessionRunnerService, and the PlanProfilingCapture / GetProfilingPrerequisites request pairs. In their place:

  • MauiProfileCommandBuilder turns a capture request into a maui profile argument list. Startup captures get --stopping-event-provider-name Microsoft.Maui.ProfilingHelper --stopping-event-event-name StartupComplete so the CLI stops on its own.
  • MauiProfilingCliService runs the process and streams --json events through MauiCliJsonStreamParser, which handles the CLI interleaving plain log lines with NDJSON envelopes on the same stream.
  • MauiCliToolService resolves the tool (MauiCliExecutableResolver), reports the installed version, and checks NuGet for updates. The Profiling page now shows a MAUI CLI status chip in the top right, matching how the .NET SDK Manager surfaces dotnetup. It offers install when the tool is missing and update when a newer prerelease is available.
  • ProfilingArtifactClassifier and ProfilingSessionStorageService (moved into Core so it is testable) handle what lands on disk.

The wizard became a single ProfilingCaptureModal: project, capture mode, target, output format, all on one screen with the CLI status inline.

Worth a look

Artifact recovery is a workaround, not a design choice. maui profile currently writes every artifact successfully and then crashes serializing its own result, because MauiCliJsonContext is missing [JsonSerializable(typeof(MauiProfileResult))]. It exits 1 and prints an E1001 envelope on stdout with empty stderr, so a naive exit-code check throws away a perfectly good capture. MauiProfileArtifactRecovery inspects the output directory on failure and salvages the session when the artifacts are actually there. Still reproducible in 0.1.0-preview.12.26368.2; this should come out once it is fixed upstream.

Two unrelated fixes came along in the second commit, both pre-existing:

  • MauiSherpa.Cli did not compile against System.CommandLine 2.0.10 (GA), which removed CommandLineConfiguration. The entry point is now root.Parse(args).InvokeAsync(). One line, and Program.cs was the only break.
  • The macOS toolbar's shared create button read "New Secret" on every page. BlazorContentPage builds one superset of toolbar items and toggles visibility per page, so every page inherited the superset default. Assigning ToolbarItem.Text after construction does nothing because MAUI's macOS handler does not observe it, so the label is now written onto the native NSToolbarItem. Verified across all eight pages sharing the action.

Testing

498 Core tests passing. The deleted pipeline's tests were replaced with coverage for each new service, including the command builder's argument shaping, the JSON stream parser's mixed-content handling, and artifact recovery against the upstream crash.

Capture was exercised end to end against an Android emulator producing MIBC and speedscope output.

Redth and others added 3 commits July 28, 2026 17:37
Sherpa owned a large custom capture stack: prerequisite checks, diagnostic
port and dsrouter planning, build/launch command generation, a multi-process
runner and a five-step wizard. `maui profile` now owns that lifecycle, so
replace the engine with a thin, typed integration over the global tool and
keep only what Sherpa uniquely provides: guided UX, session history,
artifact import/export, viewers and analysis.

Capture engine
- Add typed CLI contracts: tool status/update info, device records, profile
  request/result, lifecycle state and the canonical error envelope.
- Add a deterministic command builder and a tolerant JSON stream parser that
  survives command echo, multi-line documents and unknown/additive fields.
- Resolve the global tool shim explicitly before falling back to PATH, since
  GUI processes do not inherit shell PATH.
- Add interactive stdin to IProcessExecutionService so Interaction mode's
  begin/stop newlines are distinct from cancellation.

Capture UX
- Replace the wizard with a single Configure/Running/Result modal covering
  project, mode, running target, output format and collapsed advanced options.
- Support Startup and Interaction modes and nettrace/speedscope/mibc output.
- Scope targets to running Android devices/emulators and booted iOS simulators.
- Add a MAUI CLI status indicator to the Profiling page, mirroring the
  dotnetup indicator, with a details sheet that installs or updates the tool.
  Installs pin the resolved version because the package is prerelease-only.

Work around an upstream CLI defect
`maui profile` writes every artifact and then crashes serializing its own
result, because MauiCliJsonContext lacks JsonSerializable for
MauiProfileResult. It exits non-zero and prints an E1001 envelope on stdout
with an empty stderr, so neither the exit code nor stderr can be trusted.
Recover the result from the output folder, but only when the CLI produced no
profile and either reported nothing or reported exactly that serialization
failure. Genuine failures and cancellations still fail loudly. Reproduced on
0.1.0-preview.12.26368.2, so the recovery is not yet removable.

Persistence
- Move ProfilingSessionStorageService into Core and treat the CLI's result
  paths as authoritative instead of scanning for extensions.
- Commit a session only after a valid result and a non-empty artifact, and
  discard pending directories on failure or abort.
- Centralize artifact classification, add the MIBC kind and drop the
  regenerable multi-megabyte .etlx index on save.
- Keep the manifest additive so existing sessions and archives still load.

Remove the orchestration, runner and prerequisite layers along with their
models, requests, handlers, registrations and command-plan tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5c34731b-bd7f-47de-b52b-ad36257ab3b4
MauiSherpa.Cli did not compile against System.CommandLine 2.0.10 (GA),
which removed CommandLineConfiguration in favour of ParserConfiguration
plus InvocationConfiguration. The entry point is now
`command.Parse(args).InvokeAsync()`. Program.cs was the only break; the
SetAction-based command definitions already match the GA API.

The macOS toolbar's shared "create" button always read "New Secret"
regardless of the active page. BlazorContentPage builds a superset of
toolbar items once and toggles their visibility per page, so every page
inherited the superset's default label. Assigning ToolbarItem.Text after
construction has no effect because MAUI's macOS toolbar handler does not
observe the property, so UpdateToolbarVisibility now writes the page's
registered label straight onto the native NSToolbarItem (Label,
PaletteLabel and ToolTip) in the same loop that already toggles enabled
state.

Verified via the accessibility API across all pages sharing the action:
Create Emulator, Create Keystore, Create Certificate, Register Bundle ID,
Register Device, Create Profile, New Profile and Capture Profile.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5c34731b-bd7f-47de-b52b-ad36257ab3b4
Both branches independently added standard input support to
IProcessExecutionService: main added SendInputAsync for dotnetup's
interactive prompts (#218), and this branch added WriteInputAsync to
drive the MAUI CLI's start/stop recording prompts.

Converged on main's SendInputAsync and removed WriteInputAsync. Main's
version is the better contract: it returns false rather than throwing
when the process can no longer accept input, requires callers to opt in
via ProcessRequest.AcceptsStandardInput, rejects elevated processes, and
serializes writes against Cancel() and process disposal so stdin cannot
be closed mid-write.

MauiProfilingCliService now sets AcceptsStandardInput for interaction
captures and sends Environment.NewLine through SendInputAsync, throwing
if the CLI has stopped accepting input.

ProcessExecutionService keeps the improvements from both sides: main's
input serialization plus this branch's output-builder locking and the
Cancel() hardening that kills the process tree instead of only
cancelling the linked token source.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 5c34731b-bd7f-47de-b52b-ad36257ab3b4
@Redth
Redth merged commit 6266f2f into main Jul 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant